﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace CrossEntropyProject.Cross_Entropy
{
    public static class CrossEntropy
    {

        #region Class variables

        internal static MainForm mainForm = DataSet.mainForm;
        internal static Int32 numberOfClusters;
        internal static bool useUnionNeliteSamplesFlag = true;
        internal static bool saveIterationValuesToFileFlag = false;

        internal static int numberOfSamples;
        internal static int sampleSize;
        internal static double proportionNEliteSamples;
        internal static double epsilon;

        internal static int numOfMedoids;
        internal static List<Sample> samples;
        internal static List<CustomersCluster> clusters;

        #endregion

        #region StartCrossEntropyMethod
        internal static void StartCrossEntropyMethod()
        {

            List<KmeansIterationResult> afterNelitIterationResults = new List<KmeansIterationResult>();
            List<KmeansIterationResult> afterSortingSamplesResults = new List<KmeansIterationResult>();

            int numOfCustomers = DataSet.numOfCustomers;
            double[][] iterationCentroids = null;
            double RLastIteration = double.MaxValue;
            double RCurIteration = double.MaxValue;

            bool continueFlag = true;
            numOfMedoids = PAM.k;
            int iterationCounter = 1;

            samples = new List<Sample>(); //List that holds all the samples

            numberOfClusters = Constants.defaultNumberOfClustersForKmeans;
            numberOfSamples = Constants.defaultNumberOfSamplesForCE;
            sampleSize = Constants.defaultSampleSize;
            proportionNEliteSamples = Constants.defaultProportionNEliteSize;
            epsilon = Constants.defaultEpsilonSize;


            while (continueFlag)
            {
                    if (mainForm.backgroundWorker3.CancellationPending)
                    {
                        Constants.crossEntropyCancelFlag = true;
                        break;
                    }

                    samples.Clear();
                    UpdateLogBox("Iteration " + iterationCounter + " :" + "generating samples...");

                    //create the samples for the cross-entropy method
                    GenerateSamples(sampleSize, numberOfSamples, DataSet.transformedCustomers, DataSet.customerIDarr);



                    KMeansResults kmeansResult;
                    //Run Kmeans on each sample 
                    UpdateLogBox("Iteration " + iterationCounter + " :" + "running Kmeans on each sample...");

                    #region Run Kmeans on each sample
                    for (int i = 0; i < numberOfSamples; i++)
                    {
                        if (mainForm.backgroundWorker3.CancellationPending)
                        {
                            Constants.crossEntropyCancelFlag = true;
                            break;
                        }
                        UpdateLogBox("        running Kmeans on sample " + (i + 1).ToString());
                        kmeansResult = MyKmeans.Cluster(samples[i].Objects.ToArray(), numberOfClusters, iterationCentroids);

                        for (int j = 0; j < kmeansResult.Centroids.Length; j++)
                        //copy the centroids from the kmeans result to the sample
                        {
                            samples[i].Centroids.Add(kmeansResult.Centroids[j]);
                        }

                        samples[i].ObjectiveFunctionValue = CalculateObjectiveFunction_R(DataSet.transformedCustomers, kmeansResult.Centroids);
                    } 
                    #endregion

                    if (mainForm.backgroundWorker3.CancellationPending)
                    {
                        Constants.crossEntropyCancelFlag = true;
                        break;
                    }
                    samples.Sort();//sort the samples by the objective function value

                    #region this part is for debuging only...
                        KmeansIterationResult kir = new KmeansIterationResult(numberOfClusters, numOfMedoids);
                        kir.objFuncVal = samples[0].ObjectiveFunctionValue;
                        kir.iteration = iterationCounter;

                        for (int i = 0; i < samples[0].Centroids.Count; i++)
                        {
                            Array.Copy(samples[0].Centroids[i], kir.centroids[i], samples[0].Centroids[i].Length);
                        }
                        afterSortingSamplesResults.Add(kir); 
                    #endregion
   
                    RCurIteration = samples[0].ObjectiveFunctionValue; //save the minimum R value of current iteration

                UpdateLogBox("Iteration " + iterationCounter + " :" + "minimum R value is " + (RCurIteration/numOfCustomers).ToString());

                                /* (Math.Round(RCurIteration/numOfCustomers, 3, MidpointRounding.AwayFromZero)).ToString());*/

                    //Number of Nelite samples (the best samples results)
                    int num = Convert.ToInt32(Math.Round(proportionNEliteSamples*samples.Count, 0, MidpointRounding.AwayFromZero));
                    if (num == 0)
                        num = 1;

                    List<Sample> nEliteSamples = samples.GetRange(0, num); //Select the Nelite samples

                    if (Math.Abs((RCurIteration / numOfCustomers) - (RLastIteration / numOfCustomers))  < epsilon)
                    {
                        #region Stop cross-entropy 
                        UpdateLogBox("========================================");
                        UpdateLogBox("Stopping criterion : RCurIteration - RLastIteration = "
                            + (Math.Round(Math.Abs((RCurIteration / numOfCustomers) - (RLastIteration / numOfCustomers)), 7, MidpointRounding.AwayFromZero)).ToString());

                        clusters = new List<CustomersCluster>();

                        for (int i = 0; i < numberOfClusters; i++) //memory allocation
                            clusters.Add(new CustomersCluster());

                        BuildCustomerClusters(DataSet.transformedCustomers, DataSet.customerIDarr, nEliteSamples[0].Centroids.ToArray());
                        continueFlag = false; 
                        #endregion
                    }
                    else
                    {
                        RLastIteration = RCurIteration;

                        if (useUnionNeliteSamplesFlag)
                        {
                          #region Use Union on N-elite samples

                            Sample UnionOfNeliteSamples = new Sample();

                            UpdateLogBox("Iteration " + iterationCounter + " :" + "joining N elite samples... ");
                            // do Union of samples...
                            foreach (var sample in nEliteSamples)
                            {
                                foreach (var item in sample.Objects)
                                    UnionOfNeliteSamples.Objects.Add(item);
                            }
                            //check if backgroundWorker was cancelled
                            if (mainForm.backgroundWorker3.CancellationPending)
                            {
                                Constants.crossEntropyCancelFlag = true;
                                break;
                            }
                            UpdateLogBox("Iteration " + iterationCounter + " :" + "running Kmeans on union of N elite samples... ");
                            //run Kmeans on the Union of N-elite samples
                            kmeansResult = MyKmeans.Cluster(UnionOfNeliteSamples.Objects.ToArray(), numberOfClusters,iterationCentroids);
                            iterationCentroids = kmeansResult.Centroids;

                            #region this part is for debuging only...
                                KmeansIterationResult ki = new KmeansIterationResult(numberOfClusters, numOfMedoids);
                                ki.objFuncVal = CalculateObjectiveFunction_R(DataSet.transformedCustomers, kmeansResult.Centroids);
                                ki.iteration = iterationCounter;

                                for (int i = 0; i < kmeansResult.Centroids.Length; i++)
                                {
                                    Array.Copy(kmeansResult.Centroids[i], ki.centroids[i], kmeansResult.Centroids[i].Length);
                                }
                                afterNelitIterationResults.Add(ki);
                            #endregion

                            StringBuilder sb = new StringBuilder();
                            double[] tmpArr = new double[iterationCentroids[0].Length];

                            UpdateLogBox("Iteration " + iterationCounter + " :" + "centroids of the union of N elite samples:  ");
                            for (int i = 0; i < iterationCentroids.Length; i++)
                            {
                                Array.Copy(iterationCentroids[i], tmpArr, iterationCentroids[i].Length);
                                for (int j = 0; j < tmpArr.Length; j++)
                                {
                                    tmpArr[j] = Math.Round(tmpArr[j], 2, MidpointRounding.AwayFromZero);
                                }

                                UpdateLogBox("        " + "Centroid " + (i + 1).ToString() + " : <" + String.Join(" , ", tmpArr) + ">");
                                Array.Clear(tmpArr, 0, tmpArr.Length);
                            }

                            string str = "Iteration " + iterationCounter + " :" + "joining N elite samples... ";

                            sb.Clear();
                            for (int i = 0; i < str.Length; i++)
                            {
                                sb.Append("=");
                            }
                            UpdateLogBox(sb.ToString()); 
                         #endregion
                        }
                        else //use mean on N-elite samples
                        {
                          #region Use mean on N-elite samples
                            double[][] meanCentroids = MyKmeans.AllocateMemory(numberOfClusters, numOfMedoids);

                            UpdateLogBox("Iteration " + iterationCounter + " :" + "Calculating mean of N elite centroids... ");
                            //do the mean in two stages
                            foreach (var sample in nEliteSamples)
                            {
                                for (int i = 0; i < sample.Centroids.Count; i++)
                                {
                                    for (int j = 0; j < sample.Centroids[i].Length; j++)
                                    {
                                        meanCentroids[i][j] += sample.Centroids[i][j];
                                    }

                                }
                            }

                            for (int i = 0; i < meanCentroids.Length; i++)
                            {
                                for (int j = 0; j < meanCentroids[i].Length; j++)
                                {
                                    meanCentroids[i][j] /= nEliteSamples.Count;
                                }

                            }

                            iterationCentroids = meanCentroids;

                            #region this part is for debuging only...
                                KmeansIterationResult ki = new KmeansIterationResult(numberOfClusters, numOfMedoids);
                                ki.objFuncVal = CalculateObjectiveFunction_R(DataSet.transformedCustomers, meanCentroids);
                                ki.iteration = iterationCounter;

                                for (int i = 0; i < meanCentroids.Length; i++)
                                {
                                    Array.Copy(meanCentroids[i], ki.centroids[i], meanCentroids[i].Length);
                                }
                                afterNelitIterationResults.Add(ki);
                            #endregion

                            String str = "Iteration " + iterationCounter + " :" +
                                         "centroids of the N elite samples mean :  ";
                            UpdateLogBox(str);
                            double[] tmpArr = new double[iterationCentroids[0].Length];
                            for (int i = 0; i < iterationCentroids.Length; i++)
                            {
                                Array.Copy(iterationCentroids[i], tmpArr, iterationCentroids[i].Length);
                                for (int j = 0; j < tmpArr.Length; j++)
                                {
                                    tmpArr[j] = Math.Round(tmpArr[j], 2, MidpointRounding.AwayFromZero);
                                }

                                UpdateLogBox("        " + "Centroid " + (i + 1).ToString() + " : <" + String.Join(" , ", tmpArr) + ">");
                                Array.Clear(tmpArr, 0, tmpArr.Length);
                            }
                            StringBuilder sb = new StringBuilder();
                            sb.Clear();
                            for (int i = 0; i < str.Length; i++)
                            {
                                sb.Append("=");
                            }
                            UpdateLogBox(sb.ToString()); 

                            #endregion
                        }//else
                    }//else

                 iterationCounter++;
            }//while
            
            if (!Constants.crossEntropyCancelFlag)
            {
                Profiles.BuildRegularProfiles();  
            }

            if (saveIterationValuesToFileFlag && !Constants.crossEntropyCancelFlag)
            {
                CreateResultsFile(afterSortingSamplesResults, afterNelitIterationResults);
            }        
        }
        #endregion

        #region GenerateSamples(Int32 sampleSize, Int32 numOfSamples, double[][] dataSet)

        /// <summary>
        /// This function generate the samples needed for the cross-entropy method
        /// </summary>
        /// <param name="sampleSize"></param>
        /// <param name="numOfSamples"></param>
        /// <param name="dataSet">the transformed customers vectors</param>
        /// <param name="customersID">the id's of the transformed customers</param>
        /// <param name="customerIDarr"></param>
        /// <returns></returns>
        internal static void GenerateSamples(Int32 sampleSize, Int32 numOfSamples, double[][] dataSet, int[] customerIDarr)
        {
            int[] selectedFlagsArray = new int[dataSet.Length];  //flags array for marking which customer selected to sample

            Random rnd = new Random();
            int selectedRandomNum = 0;
            int index = 0;
            int numOfCustomers = dataSet.Length;

            //allocate memory
            for (int i = 0; i < numOfSamples; i++)
            {
                samples.Add(new Sample());
            }

            for (int i = 0; i < samples.Count; i++)
            {
                for (int j = 0; j < sampleSize; j++)
                {
                    selectedRandomNum = rnd.Next(0, numOfCustomers - 1);//select customers index which not selected yet
                    samples[i].Objects.Add(dataSet[selectedRandomNum]);
                }
            }
        }
        #endregion

        #region CalculateObjectiveFunction_R(double[][] dataSet, double[][] centroids)
        /// <summary>
        /// This function calculate the sum of distances for all the clusters represented by the set of centroids
        /// </summary>
        /// <returns>returns the value of the objective function for the current set of centroids</returns>
        internal static double CalculateObjectiveFunction_R(double[][] dataSet, double[][] centroids)
        {
            // for each object in the dataset check to which centroid it is the nearest and add the minimal distance to the totalCost
            double totalCost = 0;
            double temp;

            for (int i = 0; i < dataSet.Length; i++)
            {
                double minDistance = double.MaxValue;

                for (int j = 0; j < centroids.Length; j++)
                {
                    temp = CalculateDistance(dataSet[i], centroids[j]);
                    if (temp < minDistance)
                    {
                        minDistance = temp;
                    }
                }
                totalCost += minDistance;
            }

            return totalCost;
        }
        #endregion

        #region BuildCustomerClusters

        internal static void BuildCustomerClusters(double[][] dataSet, int[] customerIDs, double[][] centroids)
        {
            // for each object in the dataset check to which centroid it is the nearest and add the minimal distance to the totalCost

            double temp;
            int indexOfcentroid = -1;

            for (int i = 0; i < dataSet.Length; i++)
            {
                double minDistance = double.MaxValue;

                for (int j = 0; j < centroids.Length; j++)
                {
                    temp = CalculateDistance(dataSet[i], centroids[j]);
                    if (temp < minDistance)
                    {
                        minDistance = temp;
                        indexOfcentroid = j;
                    }
                }
                //assigh each customer to appropriate cluster
                clusters[indexOfcentroid].customers.Add(dataSet[i]);

                clusters[indexOfcentroid].customersIDs.Add(customerIDs[i]);

                clusters[indexOfcentroid].sumOfDistances += minDistance;

                if (clusters[indexOfcentroid].centroid.Count == 0)
                    clusters[indexOfcentroid].centroid.Add(centroids[indexOfcentroid]);

            }

        }
        #endregion

        #region CalculateDistance(double[] point, double[] centroid)
        /// <summary>
        ///  Calculates the eculidean distance from the <see cref="point"/> to the <see cref="centroid"/>
        /// </summary>
        internal static double CalculateDistance(double[] point, double[] centroid)
        {
            // For each attribute calculate the squared difference between the centroid and the point
            double sum = 0;
            for (int i = 0; i < point.Length; i++)
                sum += Math.Pow(centroid[i] - point[i], 2);

            return sum; // Math.Sqrt(sum);
        }
        #endregion

        #region UpdateLogBox(String msg)
        internal static void UpdateLogBox(String msg)
        {
            Object message = Environment.NewLine + msg;
            mainForm.backgroundWorker3.ReportProgress(0, message);
        } 
        #endregion

        #region CreateResultsFile()
        private static void CreateResultsFile(List<KmeansIterationResult> afterSortingSamplesResults, List<KmeansIterationResult> afterNelitIterationResults)
        {
            String path = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\CrossEntropy Log results " +
                            (Constants.defaultProportionNEliteSize * 100).ToString() + @"% .txt";
            StreamWriter sw = new StreamWriter(path);

            sw.WriteLine("Input File :" + Path.GetFileName(Constants.inputFileName));
            sw.WriteLine();
            String st = "Cross Entropy Parameters:";
            StringBuilder s = new StringBuilder();

            for (int i = 0; i < st.Length; i++)
            {
                s.Append("=");
            }
            sw.WriteLine(s);
            sw.WriteLine(st);
            sw.WriteLine(s);
            if (useUnionNeliteSamplesFlag)
            {
                sw.WriteLine("The Union of N elite samples was used");
            }
            else
            {
                sw.WriteLine("The Mean of N elite samples was used"); 
            }

            sw.WriteLine("Number of clusters:" + Constants.defaultNumberOfClustersForKmeans.ToString());
            sw.WriteLine("Number of samples: " + Constants.defaultNumberOfSamplesForCE.ToString());
            sw.WriteLine("Sample size: " + Constants.defaultSampleSize.ToString());
            sw.WriteLine("Proportion N Elite samples: " + (Constants.defaultProportionNEliteSize * 100).ToString() + "%");
            sw.WriteLine("Epsilon: " + Constants.defaultEpsilonSize.ToString());
            sw.WriteLine();
            sw.WriteLine(s);
            sw.WriteLine("Values after sorting the samples:");
            sw.WriteLine(s);
            sw.WriteLine();
            foreach (var VARIABLE in afterSortingSamplesResults)
            {
                sw.WriteLine("iteration " + VARIABLE.iteration + ": ");
                sw.WriteLine("objective function value: " + VARIABLE.objFuncVal );
                sw.WriteLine("Centroids:");

                for (int i = 0; i < VARIABLE.centroids.Length; i++)
                {
                    sw.WriteLine("< " + String.Join(" , ", VARIABLE.centroids[i]) + " >");
                }            
                sw.WriteLine("------------");
            }
            sw.WriteLine();
            sw.WriteLine(s);
            if (useUnionNeliteSamplesFlag)
            {
                sw.WriteLine("Values after running kmeans on N elite samples:");
            }
            else
            {
                sw.WriteLine("Values after calculating means of N elite samples:");
            }
                 
            sw.WriteLine(s);
            sw.WriteLine();

            foreach (var VARIABLE in afterNelitIterationResults)
            {
                sw.WriteLine("iteration " + VARIABLE.iteration + ": ");
                sw.WriteLine("objective function value: " + VARIABLE.objFuncVal);
                sw.WriteLine("Centroids:");

                for (int i = 0; i < VARIABLE.centroids.Length; i++)
                {
                    sw.WriteLine("< " + String.Join(" , ", VARIABLE.centroids[i]) + " >");
                }
                sw.WriteLine("------------");
            }

            sw.Close();
        } 
        #endregion

    }//class

    #region class Sample
    class Sample : IComparable<Sample>
    {

        #region variables
        internal List<double[]> Objects;

        //internal List<int> ObjectsIDs;

        internal double ObjectiveFunctionValue;

        internal List<double[]> Centroids;
        #endregion

        #region constructor
        public Sample()
        {
            this.Objects = new List<double[]>();
            this.Centroids = new List<double[]>();
           // this.ObjectsIDs = new List<int>();
            this.ObjectiveFunctionValue = 0;
        }
        #endregion

        #region IComparable<Sample> override
        public int CompareTo(Sample value)
        {
            return this.ObjectiveFunctionValue.CompareTo(value.ObjectiveFunctionValue);
        }
        #endregion

    }//class 
    #endregion

    #region class CustomersCluster
    internal class CustomersCluster : IComparable<CustomersCluster>
    {
        internal List<double[]> customers;
        internal List<int> customersIDs;
        internal double sumOfDistances;
        internal List<double[]> centroid;

        public CustomersCluster()
        {
            this.customers = new List<double[]>();
            this.centroid = new List<double[]>();
            this.customersIDs = new List<int>();
            this.sumOfDistances = 0;
        }

        public int CompareTo(CustomersCluster value)
        {
            return this.sumOfDistances.CompareTo(value.sumOfDistances);
        }

    }//class 
    #endregion

}//namespace
